本日核心價值 (Core Focus): 用可重現的 Token 計量、確定性 Prompt 快取、模型分級與 Context 截斷,把 LLM 工作流的單次成本與延遲變成可觀測、可預算的工程指標,而不是月底才看帳單。
概念說明與實戰情境 (Overview)
接上 Day 21 的防禦層之後,每次請求都可能多帶系統政策、分隔符與 Schema,Context 會膨脹。成本通常不是「模型很貴」單一原因,而是四件事疊加:重複送同一段長 system prompt、用旗艦模型做分類、把整份文件塞進視窗、以及沒有依請求記帳。優化順序應是:先量 Token,再把不變的前綴快取,再用便宜模型做分流,最後才對長文件做排序截斷。效能(延遲)與成本往往同向:較短的輸入、較小的模型、可批次的工作,三者同時改善帳單與 p95。
關鍵操作與範例 (Implementation & Example)
1. 先量 Token,再談省錢
OpenAI 相容模型常用 tiktoken。沒裝套件時,可用粗估:英文約 4 字元 1 token,繁中常接近 1–2 字 1 token,程式碼介於兩者之間。粗估只供開發期告警,上線計費仍以 API 回傳的 usage 為準。
from __future__ import annotations
import hashlib
import json
import time
from dataclasses import dataclass, asdict
try:
import tiktoken
_ENC = tiktoken.get_encoding("o200k_base")
except Exception: # pragma: no cover - fallback when tiktoken missing
_ENC = None
def count_tokens(text: str) -> int:
if _ENC is not None:
return len(_ENC.encode(text or ""))
# Approximate: CJK closer to 1 token/char; ASCII ~4 chars/token.
if not text:
return 0
cjk = sum(1 for ch in text if "\u4e00" <= ch <= "\u9fff")
other = len(text) - cjk
return cjk + max(1, other // 4)
@dataclass
class RequestCostLog:
request_id: str
model: str
input_tokens: int
output_tokens: int
cached_input_tokens: int
latency_ms: int
usd_estimate: float
cache_hit: bool
# Replace with your team's current price sheet; numbers are placeholders.
PRICE_PER_1M = {
"gpt-4.1-mini": {"input": 0.40, "cached_input": 0.10, "output": 1.60},
"gpt-4.1": {"input": 2.00, "cached_input": 0.50, "output": 8.00},
}
def estimate_usd(model: str, input_tokens: int, output_tokens: int, cached: int = 0) -> float:
p = PRICE_PER_1M[model]
billed_input = max(0, input_tokens - cached)
return (
billed_input * p["input"]
+ cached * p["cached_input"]
+ output_tokens * p["output"]
) / 1_000_000
_PROMPT_CACHE: dict[str, str] = {}
def cache_key(model: str, messages: list[dict[str, str]]) -> str:
blob = json.dumps({"model": model, "messages": messages}, ensure_ascii=False, sort_keys=True)
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
def complete_with_local_cache(model: str, messages: list[dict[str, str]], call_api) -> tuple[str, RequestCostLog]:
"""Cache bit-identical prompts (classification, routing, lint-style checks)."""
key = cache_key(model, messages)
t0 = time.perf_counter()
if key in _PROMPT_CACHE:
text = _PROMPT_CACHE[key]
cached = True
in_tok = count_tokens("".join(m["content"] for m in messages))
out_tok = count_tokens(text)
usd = 0.0
else:
text = call_api(model, messages)
_PROMPT_CACHE[key] = text
cached = False
in_tok = count_tokens("".join(m["content"] for m in messages))
out_tok = count_tokens(text)
usd = estimate_usd(model, in_tok, out_tok, cached=0)
log = RequestCostLog(
request_id=key[:12],
model=model,
input_tokens=in_tok,
output_tokens=out_tok,
cached_input_tokens=in_tok if cached else 0,
latency_ms=int((time.perf_counter() - t0) * 1000),
usd_estimate=usd,
cache_hit=cached,
)
print(json.dumps(asdict(log), ensure_ascii=False))
return text, log
2. 兩層 Caching:應用層確定性快取,與供應商 Prompt Caching
應用層快取適合輸入完全相同的工作:意圖分類、語言偵測、固定 Schema 的 lint、同一份 OpenAPI 的摘要。Key 必須包含 model、訊息本體與溫度;temperature=0 才值得快取。不要快取含不可信 HTML 的原始請求(Day 21):應快取「抽完純文字 + 截斷後」的正規化結果。
供應商 Prompt Caching 是另一層:把長且穩定的前綴(系統政策、工具定義、Schema)放在訊息最前面,變動的使用者問題與檢索片段放最後。前綴重複出現時,供應商可能對 cached input tokens 計較低單價。實務要點是「前綴位元組級穩定」:多一個空白、隨機 request id 塞進 system,快取就斷。不要把時間戳、trace id 寫進 system;那些放 header 或最後一則 user message。
3. Model Selection:分類走便宜模型,寫程式走強模型
| 工作 | 建議模型級距 | 原因 |
|---|---|---|
| 意圖分類、路由、語言偵測、是否需要 RAG | 便宜 / mini | 高流量、輸出短、錯了可用規則或二次確認 |
| 每日摘要、郵件草稿、JSON 抽取 | 中階 | 要穩的 Structured Output,但不需深推理 |
| 產生或修改程式、複雜 Debug、架構取捨 | 旗艦 / 強模型 | 需要較長推理與較低幻覺成本 |
| 大量離線標註、回填 embedding 前的清理 | Batch API + 便宜模型 | 可等數分鐘到數小時,單價較低 |
分流本身也可模型化:先用 mini 輸出 { "task": "classify|codegen|rag" },再把原請求轉給對應模型。分類 Prompt 保持確定性並走應用層快取,這條路徑的邊際成本會接近零。
4. 截斷 Context:先排序,再切長度
Token 上限不是「從尾巴砍」。對 RAG 或長 log,先用檢索分數或規則權重排序,保留 top-k 與必要 citation,其餘丟棄。Day 21 的 MAX_UNTRUSTED_CHARS 同時是安全與成本控制。實作時分別預算:system 政策固定 N token、檢索 M token、使用者問題 P token,超標只砍檢索,不砍政策。
5. Batch 與每請求記帳
非同步工作(夜間產生 Release Notes、回填分類、大量文件摘要)應走 Batch,而不是在 HTTP 請求裡同步打旗艦模型。無論同步或 Batch,都要把 usage.input_tokens、usage.output_tokens、cached tokens、model、latency 寫進結構化 log。沒有 per-request 成本,Model Selection 表只是感覺,無法做回歸。建議每筆至少含:request_id、model、task(classify / codegen / rag)、input_tokens、output_tokens、cached_input_tokens、usd_estimate、cache_hit、latency_ms。用這些欄位才能回答「分類是否該繼續用 mini」而不是「這個月模型很貴」。價格表會改,PRICE_PER_1M 應當設定檔,不要寫死在多處常數。同一個分類 Prompt 若每天打一萬次且輸入位元組級相同,應用層快取的效益會大於換更便宜的模型;先看 log 裡的 cache_hit 比例,再決定要不要改價目表。
注意事項與常見失敗 (Pitfalls)
tiktoken 或粗估告警;帳單以 API usage 為準,兩者都寫進 log。本日總結 (Takeaways)
明日預告 (Next)
截斷與 top-k 已經是檢索問題的一半。下一步把知識從「整份貼進 Prompt」改成可引用的知識庫:Day 23 將構建專屬知識庫工作流 (RAG Architecture):結合 Vector Database 與 Context Retrieval。